//
//  BannerView.swift
//  FuncTest
//
//  Created by li on 2025-05-07.
//

import SwiftUI
import Combine


public struct MGBannerView<D: RandomAccessCollection, V: View>: View  where D.Element: Identifiable {
	
	private enum Forward {
		case none, next, previous
	}
	
	public init(currentIndex: Binding<Int>, data: D, content: @escaping (D.Element) -> V) {
		
		self._currentIndex = currentIndex
		self.data = data
		self.content = content
	}
	
	@EnvironmentObject var configure: MGBannerConfigure
		
	@Binding
	private var currentIndex: Int
	
	private var data: D
	
	@ViewBuilder
	private var content: (D.Element) -> V
	
	/// 是否允许拖拽
	@State
	private var isDraggingAllowed: Bool = true

	/// 拖拽位置
	@State
	private var dragSize: CGSize = CGSize.zero
	
	/// 定时器任务
	@State
	private var timerTask: Task<Void, Never>?
	
	@State
	private var proxy: GeometryProxy?
	
	/// 是否正在拖拽
	@State
	private var isDragging: Bool = false
	
	@State
	private var isAnimating: Bool = false
	
	public var body: some View {
		GeometryReader { proxy in
			
			let gesture = DragGesture()
				.onChanged { value in
					
					guard isDraggingAllowed else {
						return
					}
					
					guard !isAnimating else {
						return
					}
					
					isDragging = true
					
					dragSize = transformDragSize(value.translation, page: proxy.size)

				}
				.onEnded { value in
					
					endDrag(value, proxy: proxy)
				}

			ZStack {

				let start = -configure.cacheCount - 1
				let end = configure.cacheCount + 1 + 1
				
				ForEach(start..<end, id: \.self) { index in
					
					if let model = data(index) {
						content(model)
							.scaleEffect(transScaleValue(index, proxy: proxy))
							.offset(dragOffset(index, proxy: proxy))
							.opacity(transOpacity(index, proxy: proxy))
							.zIndex(Double(configure.cacheCount - abs(index)))
					}
				}
			}
			.highPriorityGesture(gesture, isEnabled: configure.isDraggingAllowed)
			.onAppear {
				self.proxy = proxy
			}
		}
		.mgOnchange(of: configure.isAutoLoop, initial: true) { value in
			
			if value {
				
				startTimer()
			}else {
				
				stopTimer()
			}
		}
		.mgOnchange(of: isDragging) { oldValue, newValue in
			if newValue != oldValue {
				if newValue == false {
					startTimer()
				}else {
					stopTimer()
				}
			}
		}
    }
	
	func data(_ index: Int) -> D.Element? {
		
		if !configure.isLoop {
			if currentIndex + index >= data.count || currentIndex + index < 0 {
				return nil
			}
		}
		
		let count = data.count
		guard count > 0 else {
			return nil
		}
		
		var index = currentIndex + index
		index = index % count

		if index < 0 {
			index = count + index % count
		}else {
			index = index % count
		}
		
		let startIndex = data.index(data.startIndex, offsetBy: index)
		let endIndex = data.index(startIndex, offsetBy: 1)
		
		let result = data[startIndex..<endIndex].first
		return result
	}
	
	func dragOffset(_ index: Int, proxy: GeometryProxy) -> CGSize {
		
		var width: CGFloat = 0
		var height: CGFloat = 0
		let dragLength: CGFloat = configure.direction.isHorizontal ? dragSize.width : dragSize.height
		let page = configure.direction.isHorizontal ? proxy.size.width : proxy.size.height
		let progress = dragLength / page

		var reverse: CGFloat = 0
		if dragLength > 0 {
			reverse = 1
		}else {
			reverse = -1
		}
		

		var length: CGFloat = 0
		let currentOffset = offset(index, proxy: proxy)
		let nextOffset = offset(index + 1 * Int(reverse), proxy: proxy)
		length = abs(nextOffset - currentOffset)
		
		if configure.direction.isHorizontal {
			width = currentOffset + progress * length
			
		}else {
			height = currentOffset + progress * length
		}
		

		return CGSize(width: width, height: height)
	}
	
	func offset(_ index: Int, proxy: GeometryProxy) -> CGFloat {
		
		var cacheCount = configure.cacheCount
		
		if cacheCount == 0 {
			cacheCount = 1
		}
		var center: CGFloat = 0
		
		let averageScale = (1 - configure.minScale) / CGFloat(cacheCount)
		let page = configure.direction.isHorizontal ? proxy.size.width : proxy.size.height
		
		var previousPage: CGFloat = 0
		let reverse: CGFloat = index > 0 ?  1 : -1
		
		for i in 0...abs(index) {
			center += (previousPage / 2.0) * reverse
			
			let currentPage = page - CGFloat(min(i, cacheCount)) * averageScale * page
			
			if i > 0 {
				center += configure.itemSpacing * reverse
				center += (currentPage / 2.0) * reverse
			}
			
			previousPage = currentPage
		}
				
		return center
	}
	
	func transScaleValue(_ index: Int, proxy: GeometryProxy) -> CGFloat {
		
		let page =  configure.direction.isHorizontal ? proxy.size.width : proxy.size.height
		
		let move = configure.direction.isHorizontal ? dragSize.width : dragSize.height
		var dragToNext: CGFloat = 0
		if move > 0 {
			dragToNext = 1
		}else if move < 0 {
			dragToNext = -1
		}
		
		var scale: CGFloat = 1.0
		
		let averageScale = (1 - configure.minScale) / CGFloat(configure.cacheCount)
		
		let dragProgress = abs(move) / page

		let reverse: CGFloat = CGFloat(index) * dragToNext > 0 ? -1 : 1
		if index == 0 {
			scale = 1 - averageScale * abs(dragProgress)
		}else {

			let index: CGFloat = CGFloat(abs(index)) - dragProgress * reverse
			scale = 1 - averageScale * CGFloat(min(index, CGFloat(configure.cacheCount)))
		}
		return scale
	}
	
	func transOpacity(_ index: Int, proxy: GeometryProxy) -> CGFloat {
		
		if configure.cacheCount <= 0 {
			return 1
		}

		let dragLength: CGFloat = configure.direction.isHorizontal ? dragSize.width : dragSize.height
		let page = configure.direction.isHorizontal ? proxy.size.width : proxy.size.height
		let progress = dragLength / page
		
		let average: CGFloat = (1 - configure.minOpacity) / CGFloat(configure.cacheCount)
		
		var currentOpacity: CGFloat = 1 - CGFloat(abs(index)) * average
		
		
		if index == 0 {
			currentOpacity = currentOpacity - average * abs(progress)
		}else if index > 0 {
			currentOpacity = currentOpacity - average * progress
		}else if index < 0 {
			currentOpacity = currentOpacity + average * progress
		}
		
		
		return currentOpacity
	}
	
	func transformDragSize(_ move: CGSize, page: CGSize) -> CGSize {
		
		var width = move.width
		var height = move.height
		
		if width > 0 {
			
			width = min(width, page.width)
		}else {
			width = max(width, -page.width)
		}
		
		if height > 0 {
			
			height = min(height, page.height)
		}else {
			height = max(height, -page.height)
		}
		
		return CGSize(width: width, height: height)
	}
	
	func endDrag(_ value: _ChangedGesture<DragGesture>.Value, proxy: GeometryProxy) {
		
		guard isDraggingAllowed else {
			return
		}
		
		guard !isAnimating else {
			return
		}
		
		isDraggingAllowed = false
		isAnimating = true
		var endSize: CGSize = .zero
		var duration: TimeInterval = 0.25
		
		let page = configure.direction == .horizontal ? proxy.size.width : proxy.size.height
		let isHorizontal = configure.direction == .horizontal
		let dragLength = isHorizontal ? dragSize.width : dragSize.height
		
		let progress = min(1, max(0, 1 - abs(dragLength) / page))
		duration = duration * progress
		
		
		var forward: Forward = .none
		
		if dragLength > 0 {
			if dragLength > 30 || dragLength > page / 4.0 {

				forward = .previous
				if isHorizontal {
					endSize = .init(width: page, height: 0)
				}else {
					endSize = .init(width: 0, height: page)
				}
			}
		}else if dragLength < 0 {
			if dragLength < -30 || dragLength < -page / 4.0  {

				forward = .next
				if isHorizontal {
					endSize = .init(width: -page, height: 0)
				}else {
					endSize = .init(width: 0, height: -page)
				}
			}
		}
		
		endSize = .init(width: endSize.width, height: endSize.height)
		
		var isAllowedToSwitchIndex: Bool = true
		
		if !configure.isLoop {
			if currentIndex + 1 > data.count - 1 && forward == .next {
				isAllowedToSwitchIndex = false
			}else if currentIndex - 1 < 0 && forward == .previous {
				isAllowedToSwitchIndex = false
			}
		}

		if !isAllowedToSwitchIndex {
			endSize = .zero
		}

		withAnimation(.linear(duration: duration)) {

			dragSize = endSize
		}
		
		DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
			
			var index = currentIndex
			
			if isAllowedToSwitchIndex {
				if forward == .next {
					
					index += 1
				}else if forward == .previous {
					
					index -= 1
				}
			}
			
			if index >= 0 {
				index = index % data.count
			}else {
				index = index % data.count + data.count
			}
			
			currentIndex = index
			dragSize = .zero
			isDraggingAllowed = true
			isDragging = false
			isAnimating = false
		}
	}
	
	func gotoNext() {
		
		if data.count == 0 {
			return
		}
		
		if !configure.isLoop {
			if currentIndex + 1 > data.count - 1 {
				return
			}
		}
		
		guard let proxy else {
			return
		}
		
		isAnimating = true
		
		let duration: TimeInterval = 0.25
		
		let page = configure.direction == .horizontal ? proxy.size.width : proxy.size.height
		let isHorizontal = configure.direction == .horizontal
		
		var endSize: CGSize = .zero
		if isHorizontal {
			endSize = .init(width: -page, height: 0)
		}else {
			endSize = .init(width: 0, height: -page)
		}
		
		withAnimation(.linear(duration: duration)) {

			dragSize = endSize
		}
		
		DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
			var index = currentIndex + 1
			
			if index >= 0 {
				index = index % data.count
			}else {
				index = index % data.count + data.count
			}
			currentIndex = index % data.count
			dragSize = .zero
			isDraggingAllowed = true
			isAnimating = false
		}
	}
	
	
	@MainActor
	func startTimer() {
		
		guard configure.isAutoLoop else {
			return
		}
		timerTask = Task(operation: {
			while !Task.isCancelled {
				try? await  Task.sleep(nanoseconds: 1_000_000_000 * UInt64(configure.autoLoopTimeInterval))
				if !isDragging {
					gotoNext()
				}
			}
		})
	}
	
	func stopTimer() {
		timerTask?.cancel()
		timerTask = nil
	}
}

struct MGBannerViewTest: View {
	
	@State var list: [Data] = [
		.init(color: .blue, text: "中信1"),
		.init(color: .red, text: "建行2"),
		.init(color: .green, text: "工行3"),
		.init(color: .orange, text: "农行4"),
		.init(color: .purple, text: "平安5"),
		.init(color: .gray, text: "平安6"),
		.init(color: .black, text: "邮政7"),
		.init(color: .pink, text: "光大8"),
		.init(color: .yellow, text: "普惠9"),
	]
	
	@State var currentIndex: Int = 0
	@State var width: CGFloat = 300
	@ObservedObject var config: MGBannerConfigure = .init()
	var body: some View {
		
		ZStack {
			
			MGBannerView(currentIndex: $currentIndex, data: list) { model in
				
				ZStack {
					Button {
						print("ssss")
						currentIndex += 3
					} label: {
						CardView(data: model)
					}
				}
			}
			.frame(width: width, height: 200)
			.mgBanner(config)
			.onAppear {
				config.autoLoopTimeInterval = 3
				config.direction = .horizontal
				config.isAutoLoop = true
				config.cacheCount = 1
				config.minScale = 0.8
				config.itemSpacing = 10
				config.minOpacity = 0.5
				config.isDraggingAllowed = true
				config.isLoop = false
			}
		}
		.padding(.horizontal, 20)
	}

	struct CardView: View {
		
		let data: Data
		var body: some View {
			ZStack(alignment: .top) {
				data.color
				Text(data.text)
					.font(.system(size: 22, weight: .bold))
					.foregroundColor(.white)
					.padding(.top, 40)
			}
		}
	}
	
	
	struct Data: Identifiable {
		var id: UUID = .init()
		
		var color: Color
		var text: String
	}
}

#Preview {
//	MGBannerView(viewModel: MGBannerViewModel())
	MGBannerViewTest()
}




open class MGBannerConfigure: ObservableObject {
	
	public enum Direction {
		case vertical, horizontal
		/// 是否是水平
		public var isHorizontal: Bool {
			self == .horizontal
		}
		
	}
	
	/// 缓存数量
	@Published public var cacheCount: Int = 1
	/// 是否循环
	@Published public  var isLoop: Bool = true
	/// 缩放的最小值 (中间值 为1，两边递减知道最后一个 就是该设置的值)
	@Published public  var minScale: CGFloat = 1
	/// item 间隙
	@Published public var itemSpacing: CGFloat = 12
	/// 中间值 为1，两边递减知道最后一个 就是该设置的值
	@Published public var minOpacity: CGFloat = 1
	/// 水平，还是垂直滚动
	@Published public var direction: Direction = .horizontal
	/// 是否自动循环
	@Published public var isAutoLoop: Bool = false
	/// 自动循环的时间间隔
	@Published public var autoLoopTimeInterval: TimeInterval = 3.0
	///  是否允许拖拽
	@Published public var isDraggingAllowed: Bool = true
}



struct MGBannerConfigureKey: @preconcurrency EnvironmentKey  {
	
	@MainActor static let defaultValue: MGBannerConfigure = .init()
}

extension EnvironmentValues {
	
	fileprivate var mgBannerConfigureValue: MGBannerConfigure {
		get {
			self[MGBannerConfigureKey.self]
		}
		
		set {
			self[MGBannerConfigureKey.self] = newValue
		}
	}
}

public extension View {
	/// MGBanner 配置
	public func mgBanner(_ configure: MGBannerConfigure) -> some View {
//		self.environment(\.mgBannerConfigureValue, configure)
//		environment(configure)
		environmentObject(configure)
	}
	
	public func mgOnchange<V>(of value: V, initial: Bool = false, perform action: @escaping (V) -> Void) -> some View where V: Equatable {
		modifier(MGOnchangeModifier(value: value, initial: initial, action1: action, action2: nil))
	}
	
	public func mgOnchange<V>(of value: V, initial: Bool = false, perform action: @escaping (V,V) -> Void) -> some View where V: Equatable {
		modifier(MGOnchangeModifier(value: value, initial: initial, action1: nil, action2: action))
	}

}

private struct MGOnchangeModifier<V: Equatable>: ViewModifier {
	
	init(value: V, initial: Bool, action1: ((V) -> Void)?, action2: ((V,V) -> Void)?) {
		self.value = value
		self._initial = State(initialValue: initial)
		self._storage = State(initialValue: ValueStorage(action1: action1, action2: action2))
	}
	
	let value: V
	@State var initial: Bool
	@State var hasInitial: Bool = false
	@State private var storage: ValueStorage
	func body(content: Content) -> some View {
		
		content
			.onReceive(Just(value)) { value in
				if hasInitial {
					
					if storage.oldValue != value {
						storage.onChanged(value: value)
					}
				}else {
					
					if initial {
						storage.onChanged(value: value)
					}
					
					storage.oldValue = value
					hasInitial = true
				}
			}
	}
	
	private struct ValueStorage {
		var oldValue: V?
		private var newValue: V?
		private let action1: ((V) -> Void)?
		private let action2: ((V,V) -> Void)?
		
		init(action1: ((V) -> Void)?, action2: ((V, V) -> Void)?) {
			self.action1 = action1
			self.action2 = action2
		}
		
		mutating func onChanged(value: V) {
			newValue = value
			if let action1, let newValue = newValue {
				action1(newValue)
			}
			
			if let action2, let newValue {
				if let oldValue {
					action2(oldValue, newValue)
				}else  {
					action2(newValue, newValue)
				}
			}
			
			oldValue = newValue
		}
	}
}
